﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Microsoft.SharePoint;
using Repo.Converters;
using Repo.DataAccess;
using Repo.Helpers;
using Repo.Interfaces;

namespace Repo.DataCaching
{
    /// <summary>
    /// Static class used to rebuild the cache of all items of a certain type and all items associated with this type
    /// </summary>
    public static class CacheBuilder
    {
        /// <summary>
        /// For the input type, rebuild the cache of items for this type and also all other types that it has a dependency on
        /// </summary>
        /// <param name="site"></param>
        /// <param name="type"></param>
        /// <returns></returns>
        public static IList BuildCacheForType(SPSite site, Type type)
        {
            var listOfDependentTypes = new List<Type> {type};
            LoggingHelper.DebugLog("Getting All Dependent Types for " + type.Name);
            var dependentTypes = GetAllDependentTypes(type, listOfDependentTypes);
            LoggingHelper.DebugLog(dependentTypes.Any()
                                       ? dependentTypes.Aggregate(type.Name + " has depencies on: ",
                                                                  (current, dependentType) =>
                                                                  current + (dependentType.Name + ", "))
                                       : type.Name + " has no depencies");
            var allInSituItems = GetAllItemsForType(site, listOfDependentTypes);
            LoggingHelper.DebugLog("Will now bind all dependencies");
            var boundDependencies = BindDependencies(allInSituItems);
            foreach (var boundDependency in boundDependencies)
            {
                var thisKey = KeyGenerator.GenerateKey(ContentTypeFinder.GetContentTypeFromType(boundDependency.Key).Name, null);
                Cacher.SaveToCache(thisKey, boundDependency.Value.Items);
                LastUpdatedCache.SaveLastUpdated(thisKey);
                CurrentlyUpdatingCache.DeleteUpdatingTime(thisKey);
            }
            return boundDependencies[type].Items;
        }

        /// <summary>
        /// For the input type, get all other types which are dependencies, i.e. if the input type contains a public item or collection
        /// of other ISharePointItems then return these other types.
        /// The method is recursive to capture all dependencies until an ISharePointItem is found with no other new dependencies
        /// </summary>
        /// <param name="type"></param>
        /// <param name="dependentTypes"></param>
        /// <returns></returns>
        private static List<Type> GetAllDependentTypes(Type type, List<Type> dependentTypes)
        {
            foreach (var property in type.GetProperties())
            {
                if (IsPropertyOfTypeISharePointItem(property.PropertyType) || IsPropertyOfTypeIEnumerableOfISharePointItem(property.PropertyType))
                {
                    var propertyType = IsPropertyOfTypeIEnumerableOfISharePointItem(property.PropertyType)
                                           ? property.PropertyType.GetGenericArguments()[0]
                                           : property.PropertyType;
                    LoggingHelper.DebugLog("Found a dependency for " + type.Name + " on " + propertyType.Name);
                    if (!dependentTypes.Contains(propertyType))
                    {
                        dependentTypes.Add(propertyType);
                        GetAllDependentTypes(propertyType, dependentTypes);
                    }
                }
            }
            return dependentTypes;
        }

        /// <summary>
        /// Check if the input type is assignable from ISharePointItem
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        private static bool IsPropertyOfTypeISharePointItem(Type type)
        {
            return typeof(ISharePointItem).IsAssignableFrom(type);
        }

        /// <summary>
        /// Check if the input type is an IEnumerable of items which are assignable from ISharePointItem
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        private static bool IsPropertyOfTypeIEnumerableOfISharePointItem(Type type)
        {
            return type.IsGenericType
                   && type.GetGenericTypeDefinition() == typeof(IEnumerable<>)
                   && typeof(ISharePointItem).IsAssignableFrom(type.GetGenericArguments()[0]);
        }

        /// <summary>
        /// For each type return in a dictionary a collection of in-situ items. These items hold details of items that are already in the cache,
        /// items which are not and need to be gathered and bound as appropriate and also the associated SharePoint items
        /// </summary>
        /// <param name="site"></param>
        /// <param name="dependentTypes"></param>
        /// <returns></returns>
        private static Dictionary<Type, InSituItems> GetAllItemsForType(SPSite site, IEnumerable<Type> dependentTypes)
        {
            var allInSituItems = new Dictionary<Type, InSituItems>();
            foreach (var dependentType in dependentTypes)
            {
                var inSituItems = new InSituItems();
                var associatedContentType = ContentTypeFinder.GetContentTypeFromType(dependentType);
                if (associatedContentType != null)
                {
                    LoggingHelper.DebugLog(dependentType.Name + " is associated to the content type " + associatedContentType.Name);
                    var dependencyKey = KeyGenerator.GenerateKey(associatedContentType.Name, null);
                    var itemsInCache = CacheChecker.CheckForCachedItems(dependentType, dependencyKey);
                    if (itemsInCache != null)
                    {
                        LoggingHelper.DebugLog(dependentType.Name + " has already got bound items in the cache");
                        inSituItems.Items = itemsInCache;
                        inSituItems.IsAlreadyCached = true;
                        inSituItems.IsBound = true;
                    }
                    else
                    {
                        LoggingHelper.DebugLog(dependentType.Name + " has not been bound to dependencies and then added to the cache");
                        var spListItems = Collector.CollectItems(site, associatedContentType, null);
                        LoggingHelper.DebugLog("There are " + spListItems.Count() + " items in SharePoint Lists with the content type " + associatedContentType.Name);
                        inSituItems.AssociatedSPItems = spListItems;
                        var convertedSPListItems = SPItemConverter.ConvertSharePointItemsToT(dependentType, spListItems);
                        inSituItems.Items = convertedSPListItems;
                    }
                }
                else
                {
                    LoggingHelper.DebugLog(dependentType.Name + " is no associated content type so cannot be added to the cache");
                }
                allInSituItems.Add(dependentType, inSituItems);
            }
            return allInSituItems;
        }

        /// <summary>
        /// Creates an IList of items of the input type
        /// </summary>
        /// <param name="t"></param>
        /// <returns></returns>
        private static IList CreateListOfType(Type t)
        {
            var listType = typeof(List<>);
            var constructedListType = listType.MakeGenericType(t);
            var instance = Activator.CreateInstance(constructedListType);
            return (IList)instance;
        }

        /// <summary>
        /// Bind Dependencies for all the in-situ items. Once returned all items will have there dependencies bound and can therefore be cached for reuse
        /// </summary>
        /// <param name="allInSituItems"></param>
        /// <returns></returns>
        public static Dictionary<Type, InSituItems> BindDependencies(Dictionary<Type, InSituItems> allInSituItems)
        {
            foreach (var inSituItemsForType in allInSituItems)
            {
                var type = inSituItemsForType.Key;
                var inSituItems = inSituItemsForType.Value;
                if(inSituItems.IsAlreadyCached)
                {
                    LoggingHelper.DebugLog("items of type " + type + " are already in the cache so need to bind them");
                }
                else
                {
                    var itemsToBind = inSituItems.Items;
                    for (int currentIndex=0; currentIndex< itemsToBind.Count; currentIndex++)
                    {
                        var itemToBind = itemsToBind[currentIndex];
                        foreach (var property in type.GetProperties())
                        {
                            var dependencyType = property.PropertyType;
                            if (IsPropertyOfTypeISharePointItem(dependencyType))
                            {
                                var spValue = inSituItems.AssociatedSPItems.ElementAt(currentIndex)[property.Name];
                                var split = spValue.ToString().Split(";#".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
                                int itemId;
                                if (int.TryParse((split.Length == 1 ? split.ToString() : split[0]), out itemId))
                                {
                                    var inSituItemsOfDependencyType =  allInSituItems[dependencyType];
                                    var itemsToSearchForAssociatedItem = inSituItemsOfDependencyType.Items;
                                    foreach (var item in itemsToSearchForAssociatedItem)
                                    {
                                        var id = (int)dependencyType.GetProperty("ID").GetValue(item, null);
                                        if (id == itemId)
                                        {
                                            property.SetValue(itemToBind, item, null);
                                            break;
                                        }
                                    }
                                }
                            }
                            else if (IsPropertyOfTypeIEnumerableOfISharePointItem(property.PropertyType))
                            {
                                var spValue = inSituItems.AssociatedSPItems.ElementAt(currentIndex)[property.Name];
                                dependencyType = property.PropertyType.GetGenericArguments()[0];
                                var inSituItemsOfDependencyType = allInSituItems[dependencyType];
                                var itemsToSearchForAssociatedItem = inSituItemsOfDependencyType.Items;

                                var spValueAsLookup = ((SPFieldLookupValueCollection)spValue);
                                var listOfDependencyItems = CreateListOfType(property.PropertyType.GetGenericArguments()[0]);
                                foreach (SPFieldLookupValue spFieldLookupValue in spValueAsLookup)
                                {
                                    foreach (var item in itemsToSearchForAssociatedItem)
                                    {
                                        var id = (int)dependencyType.GetProperty("ID").GetValue(item, null);
                                        if (id == spFieldLookupValue.LookupId)
                                        {
                                            listOfDependencyItems.Add(item);
                                            break;
                                        }
                                    }
                                }
                                property.SetValue(itemToBind, listOfDependencyItems, null);
                            }
                        }
                    }
                }
                inSituItems.IsBound = true;
            }
            return allInSituItems;
        }
    }
}
